// Copyright Wilfried Woivre - http://blog.woivre.fr

using System;
using System.Collections.Generic;
using System.Data.Services.Providers;
using System.Linq;

namespace Demo.TableStorageDataServices.Web.Services
{
	public class DataSourceUpdateProvider<T> : IDataServiceUpdateProvider where T : class, IDataSource
	{
        private readonly IDataServiceMetadataProvider _metadata;
        private readonly IDataServiceQueryProvider _query;
        private readonly List<Action> _actions;

        public DataSourceUpdateProvider(IDataServiceMetadataProvider metadataProvider, IDataServiceQueryProvider queryProvider)
        {
            _metadata = metadataProvider;
            _query = queryProvider;
            _actions = new List<Action>();
        }

        public T GetContext()
        {
            return (_query.CurrentDataSource as T);
        }

        #region Implementation of IUpdatable

        /// <summary>
        /// Creates the resource of the specified type and that belongs to the specified container.
        /// </summary>
        /// <returns>
        /// The object representing a resource of specified type and belonging to the specified container.
        /// </returns>
        /// <param name="containerName">The name of the entity set to which the resource belongs.</param><param name="fullTypeName">The full namespace-qualified type name of the resource.</param>
        /// <exception cref="Exception"><c>Exception</c>.</exception>
        public object CreateResource(string containerName, string fullTypeName)
        {
            ResourceType type = null;
            if (_metadata.TryResolveResourceType(fullTypeName, out type))
            {
                var context = GetContext();
                var resource = context.CreateResource(type);
                _actions.Add(() => context.AddResource(type, resource));
                return resource;
            }
            throw new Exception(string.Format("Type {0} not found", fullTypeName)
            );
        }

        /// <summary>
        /// Gets the resource of the specified type identified by a query and type name. 
        /// </summary>
        /// <returns>
        /// An opaque object representing a resource of the specified type, referenced by the specified query.
        /// </returns>
        /// <param name="query">Language integrated query (LINQ) pointing to a particular resource.</param><param name="fullTypeName">The fully qualified type name of resource.</param>
        /// <exception cref="Exception">Resource not found</exception>
        public object GetResource(IQueryable query, string fullTypeName)
        {
            var enumerator = query.GetEnumerator();
            if (!enumerator.MoveNext())
                throw new Exception("Resource not found");
            var resource = enumerator.Current;
            if (enumerator.MoveNext())
                throw new Exception("Resource not uniquely identified");

            if (fullTypeName != null)
            {
                ResourceType type = null;
                if (!_metadata.TryResolveResourceType(fullTypeName, out type))
                    throw new Exception("ResourceType not found");

                if (!type.InstanceType.IsAssignableFrom(resource.GetType()))
                    throw new Exception("Unexpected resource type");
            }

            return resource;
        }

        /// <summary>
        /// Resets the resource identified by the parameter <paramref name="resource "/>to its default value.
        /// </summary>
        /// <returns>
        /// The resource with its value reset to the default value.
        /// </returns>
        /// <param name="resource">The resource to be updated.</param>
        public object ResetResource(object resource)
        {
            _actions.Add(() => ReallyResetResource(resource));
            return resource;
        }

        public void ReallyResetResource(object resource)
        {
            // Create an new 'blank' instance of the resource 
            var clrType = resource.GetType();
            ResourceType resourceType = _metadata.Types.Single(t => t.InstanceType == clrType);
            var resetTemplate = GetContext().CreateResource(resourceType);

            // Copy non-key property values from the 'blank' resource 
            foreach (var prop in resourceType.Properties.Where(p => (p.Kind & ResourcePropertyKind.Key) != ResourcePropertyKind.Key))
            {
                // Obviously for perf reasons you could might want to 
                // cache the result of these reflection calls. 
                var clrProp = clrType.GetProperties().Single(p => p.Name == prop.Name);

                var defaultPropValue = clrProp.GetGetMethod().Invoke(resetTemplate, new object[] { });

                clrProp.GetSetMethod().Invoke(resource, new object[] { defaultPropValue });
            }
        }

        /// <summary>
        /// Sets the value of the property with the specified name on the target resource to the specified property value.
        /// </summary>
        /// <param name="targetResource">The target object that defines the property.</param><param name="propertyName">The name of the property whose value needs to be updated.</param><param name="propertyValue">The property value for update.</param>
        public void SetValue(object targetResource, string propertyName, object propertyValue)
        {
            _actions.Add(() => ReallySetValue(targetResource, propertyName, propertyValue));
        }

        public void ReallySetValue(object targetResource, string propertyName, object propertyValue)
        {
            targetResource.GetType().GetProperties().Single(p => p.Name == propertyName).GetSetMethod().Invoke(targetResource, new[] { propertyValue });
        }

        /// <summary>
        /// Gets the value of the specified property on the target object.
        /// </summary>
        /// <returns>
        /// The value of the object.
        /// </returns>
        /// <param name="targetResource">An opaque object that represents a resource.</param><param name="propertyName">The name of the property whose value needs to be retrieved.</param>
        public object GetValue(object targetResource, string propertyName)
        {
            var value = targetResource.GetType().GetProperties().Single(p => p.Name == propertyName).GetGetMethod().Invoke(targetResource, new object[] { });

            return value;
        }

        /// <summary>
        /// Sets the value of the specified reference property on the target object.
        /// </summary>
        /// <param name="targetResource">The target object that defines the property.</param><param name="propertyName">The name of the property whose value needs to be updated.</param><param name="propertyValue">The property value to be updated.</param>
        public void SetReference(object targetResource, string propertyName, object propertyValue)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Adds the specified value to the collection.
        /// </summary>
        /// <param name="targetResource">Target object that defines the property.</param><param name="propertyName">The name of the collection property to which the resource should be added..</param><param name="resourceToBeAdded">The opaque object representing the resource to be added.</param>
        public void AddReferenceToCollection(object targetResource, string propertyName, object resourceToBeAdded)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Removes the specified value from the collection.
        /// </summary>
        /// <param name="targetResource">The target object that defines the property.</param><param name="propertyName">The name of the property whose value needs to be updated.</param><param name="resourceToBeRemoved">The property value that needs to be removed.</param>
        public void RemoveReferenceFromCollection(object targetResource, string propertyName, object resourceToBeRemoved)
        {
            throw new NotImplementedException();
        }

        /// <summary>
        /// Deletes the specified resource.
        /// </summary>
        /// <param name="targetResource">The resource to be deleted.</param>
        public void DeleteResource(object targetResource)
        {
            _actions.Add(() =>GetContext().DeleteResource(targetResource)); 
        }

        /// <summary>
        /// Saves all the changes that have been made by using the <see cref="T:System.Data.Services.IUpdatable"/> APIs.
        /// </summary>
        public void SaveChanges()
        {
            foreach (var a in _actions)
                a();
            GetContext().SaveChanges(); 
        }

        /// <summary>
        /// Returns the instance of the resource represented by the specified resource object.
        /// </summary>
        /// <returns>
        /// Returns the instance of the resource represented by the specified resource object.
        /// </returns>
        /// <param name="resource">The object representing the resource whose instance needs to be retrieved.</param>
        public object ResolveResource(object resource)
        {
            return resource; 
        }

        /// <summary>
        /// Cancels a change to the data.
        /// </summary>
        public void ClearChanges()
        {
            _actions.Clear(); 
        }

        /// <summary>
        /// Supplies the eTag value for the given entity resource.
        /// </summary>
        /// <param name="resourceCookie">Cookie that represents the resource.</param><param name="checkForEquality">A <see cref="T:System.Boolean"/> that is true when property values must be compared for equality; false when property values must be compared for inequality.</param><param name="concurrencyValues">An <see cref="T:System.Collections.Generic.IEnumerable`1"/> list of the eTag property names and corresponding values.</param>
        public void SetConcurrencyValues(object resourceCookie, bool? checkForEquality, IEnumerable<KeyValuePair<string, object>> concurrencyValues)
        {
            throw new NotImplementedException();
        }

        #endregion
    }
}

